<?php

namespace App\Service;

use App\Constant\CacheKey;
use Swoole;

/**
 * Created by PhpStorm.
 * User: eric
 * Date: 5/12/16
 * Time: 11:39 PM
 */
trait HotReloadTrait
{
    // 检查下一次重新加载的时间间隔
    private $check_reload_span = [];

    // 上一次加载完成的时间
    private $last_load_at = [];

    // 正在进行加载操作的
    private $loadings = [];

    /**
     * @param $name         需要load的内容名称
     * @param $function     执行load的函数名称
     * @return mixed|void
     */
    public function doLoadIfNeed($name, $function)
    {
        if (!$this->isNeedReload($name)) {
            return;
        }

        // 加锁失败, 已经有并发开始执行更新
        if (!$this->markLoadStart($name)) {
            return;
        }

        // 进行加载操作
        $data = call_user_func([$this, $function]);

        // 标记加载结束
        $this->markLoadFinish($name);

        return $data;
    }


    /**
     * 设置需要更新数据$name
     *
     * @param $name
     */
    public function setNeedReload($name)
    {
        $key = $this->buildReloadCacheKey($name);
        Swoole::getInstance()->predis->connection()->set($key, microtime(true));
    }


    /**
     * 判断是否需要进行更新操作
     *
     * @param $name
     * @return bool
     */
    private function isNeedReload($name)
    {
        // 名称不对, 不进行加载
        if (!is_string($name)) {
            return false;
        }

        // 尚未有加载记录, 进行加载
        if (!array_key_exists($name, $this->last_load_at)) {
            return true;
        }

        // 获取判断加载的时间间隔
        $span = array_key_exists($name, $this->check_reload_span) ?: 30;

        $now = microtime(true);

        // 在一定时间范围内, 认为"不需要检查"是否需要重新加载(视为不更新)
        if ($this->last_load_at[$name] + $span > $now) {
            return false;
        }

        // 查询Redis, 获取最新的需要加载的时间点
        $key = $this->buildReloadCacheKey($name);
        $need_reload_at = Swoole::getInstance()->predis->connection()->get($key);

        // 如果最新的时间点比上一次加载时间点晚, 进行加载
        if ($need_reload_at && $need_reload_at > $this->last_load_at[$name]) {
            return true;
        }

        return false;
    }

    /**
     * 这里先判断是否有锁, 然后再加锁
     * 在极端情况下, 并不能起到加锁的作用, 不过影响不大, 重复load也OK
     *
     * @param $name
     * @return bool
     */
    private function markLoadStart($name)
    {
        // 判断是否已经有锁
        if (array_key_exists($name, $this->loadings)) {
            return false;
        }
        // 加锁
        $this->loadings[$name] = microtime(true);
        return true;
    }


    // 标记加载完成
    private function markLoadFinish($name)
    {
        // 记录本次的更新时间
        $this->last_load_at[$name] = microtime(true);
        $this->check_reload_span[$name] = $this->randSpan();

        // 解锁
        unset($this->loadings[$name]);
    }


    // 随机是一个时间间隔, 防止多台机器同一时间点去加载DB, 增加DB压力(虽然目前没啥压力)
    private function randSpan()
    {
        return rand(60, 65);
    }


    // 构建是否加载的缓存Key
    private function buildReloadCacheKey($name)
    {
        $key = CacheKey::HOT_RELOAD . $name;

        return $key;
    }
}
